/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.manager;

import org.apache.catalina.mbeans.MBeanDumper;
import org.apache.tomcat.util.modeler.Registry;

import javax.management.*;
import javax.management.openmbean.CompositeData;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Set;

/**
 * This servlet will dump JMX attributes in a simple format
 * and implement proxy services for modeler.
 *
 * @author Costin Manolache
 */
public class JMXProxyServlet extends HttpServlet {

	private static final long serialVersionUID = 1L;

	// Constant for "no parameters" when invoking a JMX operation
	// without any parameters.
	private static final String[] NO_PARAMETERS = new String[0];

	// ----------------------------------------------------- Instance Variables
	/**
	 * MBean server.
	 */
	protected transient MBeanServer mBeanServer = null;
	protected transient Registry registry;

	// --------------------------------------------------------- Public Methods

	/**
	 * Initialize this servlet.
	 */
	@Override
	public void init() throws ServletException {
		// Retrieve the MBean server
		registry = Registry.getRegistry(null, null);
		mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
	}

	/**
	 * Process a GET request for the specified resource.
	 *
	 * @param request  The servlet request we are processing
	 * @param response The servlet response we are creating
	 * @throws IOException      if an input/output error occurs
	 * @throws ServletException if a servlet-specified error occurs
	 */
	@Override
	public void doGet(HttpServletRequest request,
	                  HttpServletResponse response)
			throws IOException, ServletException {
		response.setContentType("text/plain");

		PrintWriter writer = response.getWriter();

		if (mBeanServer == null) {
			writer.println("Error - No mbean server");
			return;
		}

		String qry = request.getParameter("set");
		if (qry != null) {
			String name = request.getParameter("att");
			String val = request.getParameter("val");

			setAttribute(writer, qry, name, val);
			return;
		}
		qry = request.getParameter("get");
		if (qry != null) {
			String name = request.getParameter("att");
			getAttribute(writer, qry, name, request.getParameter("key"));
			return;
		}
		qry = request.getParameter("invoke");
		if (qry != null) {
			String opName = request.getParameter("op");
			String[] params = getInvokeParameters(request.getParameter("ps"));
			invokeOperation(writer, qry, opName, params);
			return;
		}
		qry = request.getParameter("qry");
		if (qry == null) {
			qry = "*:*";
		}

		listBeans(writer, qry);
	}

	public void getAttribute(PrintWriter writer, String onameStr, String att, String key) {
		try {
			ObjectName oname = new ObjectName(onameStr);
			Object value = mBeanServer.getAttribute(oname, att);

			if (null != key && value instanceof CompositeData)
				value = ((CompositeData) value).get(key);

			String valueStr;
			if (value != null) {
				valueStr = value.toString();
			} else {
				valueStr = "<null>";
			}

			writer.print("OK - Attribute get '");
			writer.print(onameStr);
			writer.print("' - ");
			writer.print(att);

			if (null != key) {
				writer.print(" - key '");
				writer.print(key);
				writer.print("'");
			}

			writer.print(" = ");

			writer.println(MBeanDumper.escape(valueStr));
		} catch (Exception ex) {
			writer.println("Error - " + ex.toString());
			ex.printStackTrace(writer);
		}
	}

	public void setAttribute(PrintWriter writer,
	                         String onameStr, String att, String val) {
		try {
			setAttributeInternal(onameStr, att, val);
			writer.println("OK - Attribute set");
		} catch (Exception ex) {
			writer.println("Error - " + ex.toString());
			ex.printStackTrace(writer);
		}
	}

	public void listBeans(PrintWriter writer, String qry) {

		Set<ObjectName> names = null;
		try {
			names = mBeanServer.queryNames(new ObjectName(qry), null);
			writer.println("OK - Number of results: " + names.size());
			writer.println();
		} catch (Exception ex) {
			writer.println("Error - " + ex.toString());
			ex.printStackTrace(writer);
			return;
		}

		String dump = MBeanDumper.dumpBeans(mBeanServer, names);
		writer.print(dump);
	}

	/**
	 * Determines if a type is supported by the {@link JMXProxyServlet}.
	 *
	 * @param type The type to check
	 * @return Always returns <code>true</code>
	 */
	public boolean isSupported(String type) {
		return true;
	}

	private void invokeOperation(PrintWriter writer, String onameStr, String op,
	                             String[] valuesStr) {
		try {
			Object retVal = invokeOperationInternal(onameStr, op, valuesStr);
			if (retVal != null) {
				writer.println("OK - Operation " + op + " returned:");
				output("", writer, retVal);
			} else {
				writer.println("OK - Operation " + op + " without return value");
			}
		} catch (Exception ex) {
			writer.println("Error - " + ex.toString());
			ex.printStackTrace(writer);
		}
	}

	/**
	 * Parses parameter values from a parameter string.
	 *
	 * @param paramString The string containing comma-separated
	 *                    operation-invocation parameters, or
	 *                    <code>null</code> if there are no parameters.
	 * @return An array of String parameters (empty array if
	 * <code>paramString</code> was <code>null</code>).
	 */
	private String[] getInvokeParameters(String paramString) {
		if (paramString == null)
			return NO_PARAMETERS;
		else
			return paramString.split(",");
	}

	/**
	 * Sets an MBean attribute's value.
	 */
	private void setAttributeInternal(String onameStr,
	                                  String attributeName,
	                                  String value)
			throws OperationsException, MBeanException, ReflectionException {
		ObjectName oname = new ObjectName(onameStr);
		String type = registry.getType(oname, attributeName);
		Object valueObj = registry.convertValue(type, value);
		mBeanServer.setAttribute(oname, new Attribute(attributeName, valueObj));
	}

	/**
	 * Invokes an operation on an MBean.
	 *
	 * @param onameStr   The name of the MBean.
	 * @param operation  The name of the operation to invoke.
	 * @param parameters An array of Strings containing the parameters to the
	 *                   operation. They will be converted to the appropriate
	 *                   types to call the requested operation.
	 * @return The value returned by the requested operation.
	 */
	private Object invokeOperationInternal(String onameStr,
	                                       String operation,
	                                       String[] parameters)
			throws OperationsException, MBeanException, ReflectionException {
		ObjectName oname = new ObjectName(onameStr);
		MBeanOperationInfo methodInfo = registry.getMethodInfo(oname, operation);
		MBeanParameterInfo[] signature = methodInfo.getSignature();
		String[] signatureTypes = new String[signature.length];
		Object[] values = new Object[signature.length];
		for (int i = 0; i < signature.length; i++) {
			MBeanParameterInfo pi = signature[i];
			signatureTypes[i] = pi.getType();
			values[i] = registry.convertValue(pi.getType(), parameters[i]);
		}

		return mBeanServer.invoke(oname, operation, values, signatureTypes);
	}

	private void output(String indent, PrintWriter writer, Object result) {
		if (result instanceof Object[]) {
			for (Object obj : (Object[]) result) {
				output("  " + indent, writer, obj);
			}
		} else {
			String strValue;
			if (result != null) {
				strValue = result.toString();
			} else {
				strValue = "<null>";
			}
			writer.println(indent + strValue);
		}
	}
}
